/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.util;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;

/**
 * Implementation of {@link AnnotatedOutput} which stores the written data into
 * a {@code byte[]}.
 * <p>
 * <b>Note:</b> As per the {@link Output} interface, multi-byte writes all use
 * little-endian order.
 * </p>
 */
public class ByteArrayAnnotatedOutput implements AnnotatedOutput, ByteOutput {

	/** default size for stretchy instances */
	private static final int DEFAULT_SIZE = 1000;

	/**
	 * whether the instance is stretchy, that is, whether its array may be
	 * resized to increase capacity
	 */
	private final boolean stretchy;

	/** {@code non-null;} the data itself */
	private byte[] data;

	/** {@code >= 0;} current output cursor */
	private int cursor;

	/** whether annotations are to be verbose */
	private boolean verbose;

	/**
	 * {@code null-ok;} list of annotations, or {@code null} if this instance
	 * isn't keeping them
	 */
	private ArrayList<Annotation> annotations;

	/** {@code >= 40 (if used);} the desired maximum annotation width */
	private int annotationWidth;

	/**
	 * {@code >= 8 (if used);} the number of bytes of hex output to use in
	 * annotations
	 */
	private int hexCols;

	/**
	 * Constructs an instance with a fixed maximum size. Note that the given
	 * array is the only one that will be used to store data. In particular, no
	 * reallocation will occur in order to expand the capacity of the resulting
	 * instance. Also, the constructed instance does not keep annotations by
	 * default.
	 * 
	 * @param data
	 *            {@code non-null;} data array to use for output
	 */
	public ByteArrayAnnotatedOutput(byte[] data) {
		this(data, false);
	}

	/**
	 * Constructs a "stretchy" instance. The underlying array may be
	 * reallocated. The constructed instance does not keep annotations by
	 * default.
	 */
	public ByteArrayAnnotatedOutput() {
		this(DEFAULT_SIZE);
	}

	/**
	 * Constructs a "stretchy" instance with initial size {@code size}. The
	 * underlying array may be reallocated. The constructed instance does not
	 * keep annotations by default.
	 */
	public ByteArrayAnnotatedOutput(int size) {
		this(new byte[size], true);
	}

	/**
	 * Internal constructor.
	 * 
	 * @param data
	 *            {@code non-null;} data array to use for output
	 * @param stretchy
	 *            whether the instance is to be stretchy
	 */
	private ByteArrayAnnotatedOutput(byte[] data, boolean stretchy) {
		if (data == null) {
			throw new NullPointerException("data == null");
		}

		this.stretchy = stretchy;
		this.data = data;
		this.cursor = 0;
		this.verbose = false;
		this.annotations = null;
		this.annotationWidth = 0;
		this.hexCols = 0;
	}

	/**
	 * Gets the underlying {@code byte[]} of this instance, which may be larger
	 * than the number of bytes written
	 * 
	 * @see #toByteArray
	 * @return {@code non-null;} the {@code byte[]}
	 */
	public byte[] getArray() {
		return data;
	}

	/**
	 * Constructs and returns a new {@code byte[]} that contains the written
	 * contents exactly (that is, with no extra unwritten bytes at the end).
	 * 
	 * @see #getArray
	 * @return {@code non-null;} an appropriately-constructed array
	 */
	public byte[] toByteArray() {
		byte[] result = new byte[cursor];
		System.arraycopy(data, 0, result, 0, cursor);
		return result;
	}

	/** {@inheritDoc} */
	public int getCursor() {
		return cursor;
	}

	/** {@inheritDoc} */
	public void assertCursor(int expectedCursor) {
		if (cursor != expectedCursor) {
			throw new ExceptionWithContext("expected cursor " + expectedCursor
					+ "; actual value: " + cursor);
		}
	}

	/** {@inheritDoc} */
	public void writeByte(int value) {
		int writeAt = cursor;
		int end = writeAt + 1;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		data[writeAt] = (byte) value;
		cursor = end;
	}

	/** {@inheritDoc} */
	public void writeShort(int value) {
		int writeAt = cursor;
		int end = writeAt + 2;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		data[writeAt] = (byte) value;
		data[writeAt + 1] = (byte) (value >> 8);
		cursor = end;
	}

	/** {@inheritDoc} */
	public void writeInt(int value) {
		int writeAt = cursor;
		int end = writeAt + 4;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		data[writeAt] = (byte) value;
		data[writeAt + 1] = (byte) (value >> 8);
		data[writeAt + 2] = (byte) (value >> 16);
		data[writeAt + 3] = (byte) (value >> 24);
		cursor = end;
	}

	/** {@inheritDoc} */
	public void writeLong(long value) {
		int writeAt = cursor;
		int end = writeAt + 8;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		int half = (int) value;
		data[writeAt] = (byte) half;
		data[writeAt + 1] = (byte) (half >> 8);
		data[writeAt + 2] = (byte) (half >> 16);
		data[writeAt + 3] = (byte) (half >> 24);

		half = (int) (value >> 32);
		data[writeAt + 4] = (byte) half;
		data[writeAt + 5] = (byte) (half >> 8);
		data[writeAt + 6] = (byte) (half >> 16);
		data[writeAt + 7] = (byte) (half >> 24);

		cursor = end;
	}

	/** {@inheritDoc} */
	public int writeUleb128(int value) {
		if (stretchy) {
			ensureCapacity(cursor + 5); // pessimistic
		}
		int cursorBefore = cursor;
		Leb128Utils.writeUnsignedLeb128(this, value);
		return (cursor - cursorBefore);
	}

	/** {@inheritDoc} */
	public int writeSleb128(int value) {
		if (stretchy) {
			ensureCapacity(cursor + 5); // pessimistic
		}
		int cursorBefore = cursor;
		Leb128Utils.writeSignedLeb128(this, value);
		return (cursor - cursorBefore);
	}

	/** {@inheritDoc} */
	public void write(ByteArray bytes) {
		int blen = bytes.size();
		int writeAt = cursor;
		int end = writeAt + blen;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		bytes.getBytes(data, writeAt);
		cursor = end;
	}

	/** {@inheritDoc} */
	public void write(byte[] bytes, int offset, int length) {
		int writeAt = cursor;
		int end = writeAt + length;
		int bytesEnd = offset + length;

		// twos-complement math trick: ((x < 0) || (y < 0)) <=> ((x|y) < 0)
		if (((offset | length | end) < 0) || (bytesEnd > bytes.length)) {
			throw new IndexOutOfBoundsException("bytes.length " + bytes.length
					+ "; " + offset + "..!" + end);
		}

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		System.arraycopy(bytes, offset, data, writeAt, length);
		cursor = end;
	}

	/** {@inheritDoc} */
	public void write(byte[] bytes) {
		write(bytes, 0, bytes.length);
	}

	/** {@inheritDoc} */
	public void writeZeroes(int count) {
		if (count < 0) {
			throw new IllegalArgumentException("count < 0");
		}

		int end = cursor + count;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		/*
		 * There is no need to actually write zeroes, since the array is already
		 * preinitialized with zeroes.
		 */

		cursor = end;
	}

	/** {@inheritDoc} */
	public void alignTo(int alignment) {
		int mask = alignment - 1;

		if ((alignment < 0) || ((mask & alignment) != 0)) {
			throw new IllegalArgumentException("bogus alignment");
		}

		int end = (cursor + mask) & ~mask;

		if (stretchy) {
			ensureCapacity(end);
		} else if (end > data.length) {
			throwBounds();
			return;
		}

		/*
		 * There is no need to actually write zeroes, since the array is already
		 * preinitialized with zeroes.
		 */

		cursor = end;
	}

	/** {@inheritDoc} */
	public boolean annotates() {
		return (annotations != null);
	}

	/** {@inheritDoc} */
	public boolean isVerbose() {
		return verbose;
	}

	/** {@inheritDoc} */
	public void annotate(String msg) {
		if (annotations == null) {
			return;
		}

		endAnnotation();
		annotations.add(new Annotation(cursor, msg));
	}

	/** {@inheritDoc} */
	public void annotate(int amt, String msg) {
		if (annotations == null) {
			return;
		}

		endAnnotation();

		int asz = annotations.size();
		int lastEnd = (asz == 0) ? 0 : annotations.get(asz - 1).getEnd();
		int startAt;

		if (lastEnd <= cursor) {
			startAt = cursor;
		} else {
			startAt = lastEnd;
		}

		annotations.add(new Annotation(startAt, startAt + amt, msg));
	}

	/** {@inheritDoc} */
	public void endAnnotation() {
		if (annotations == null) {
			return;
		}

		int sz = annotations.size();

		if (sz != 0) {
			annotations.get(sz - 1).setEndIfUnset(cursor);
		}
	}

	/** {@inheritDoc} */
	public int getAnnotationWidth() {
		int leftWidth = 8 + (hexCols * 2) + (hexCols / 2);

		return annotationWidth - leftWidth;
	}

	/**
	 * Indicates that this instance should keep annotations. This method may be
	 * called only once per instance, and only before any data has been written
	 * to the it.
	 * 
	 * @param annotationWidth
	 *            {@code >= 40;} the desired maximum annotation width
	 * @param verbose
	 *            whether or not to indicate verbose annotations
	 */
	public void enableAnnotations(int annotationWidth, boolean verbose) {
		if ((annotations != null) || (cursor != 0)) {
			throw new RuntimeException("cannot enable annotations");
		}

		if (annotationWidth < 40) {
			throw new IllegalArgumentException("annotationWidth < 40");
		}

		int hexCols = (((annotationWidth - 7) / 15) + 1) & ~1;
		if (hexCols < 6) {
			hexCols = 6;
		} else if (hexCols > 10) {
			hexCols = 10;
		}

		this.annotations = new ArrayList<Annotation>(1000);
		this.annotationWidth = annotationWidth;
		this.hexCols = hexCols;
		this.verbose = verbose;
	}

	/**
	 * Finishes up annotation processing. This closes off any open annotations
	 * and removes annotations that don't refer to written data.
	 */
	public void finishAnnotating() {
		// Close off the final annotation, if any.
		endAnnotation();

		// Remove annotations that refer to unwritten data.
		if (annotations != null) {
			int asz = annotations.size();
			while (asz > 0) {
				Annotation last = annotations.get(asz - 1);
				if (last.getStart() > cursor) {
					annotations.remove(asz - 1);
					asz--;
				} else if (last.getEnd() > cursor) {
					last.setEnd(cursor);
					break;
				} else {
					break;
				}
			}
		}
	}

	/**
	 * Writes the annotated content of this instance to the given writer.
	 * 
	 * @param out
	 *            {@code non-null;} where to write to
	 */
	public void writeAnnotationsTo(Writer out) throws IOException {
		int width2 = getAnnotationWidth();
		int width1 = annotationWidth - width2 - 1;

		TwoColumnOutput twoc = new TwoColumnOutput(out, width1, width2, "|");
		Writer left = twoc.getLeft();
		Writer right = twoc.getRight();
		int leftAt = 0; // left-hand byte output cursor
		int rightAt = 0; // right-hand annotation index
		int rightSz = annotations.size();

		while ((leftAt < cursor) && (rightAt < rightSz)) {
			Annotation a = annotations.get(rightAt);
			int start = a.getStart();
			int end;
			String text;

			if (leftAt < start) {
				// This is an area with no annotation.
				end = start;
				start = leftAt;
				text = "";
			} else {
				// This is an area with an annotation.
				end = a.getEnd();
				text = a.getText();
				rightAt++;
			}

			left.write(Hex.dump(data, start, end - start, start, hexCols, 6));
			right.write(text);
			twoc.flush();
			leftAt = end;
		}

		if (leftAt < cursor) {
			// There is unannotated output at the end.
			left.write(Hex.dump(data, leftAt, cursor - leftAt, leftAt, hexCols,
					6));
		}

		while (rightAt < rightSz) {
			// There are zero-byte annotations at the end.
			right.write(annotations.get(rightAt).getText());
			rightAt++;
		}

		twoc.flush();
	}

	/**
	 * Throws the excpetion for when an attempt is made to write past the end of
	 * the instance.
	 */
	private static void throwBounds() {
		throw new IndexOutOfBoundsException("attempt to write past the end");
	}

	/**
	 * Reallocates the underlying array if necessary. Calls to this method
	 * should be guarded by a test of {@link #stretchy}.
	 * 
	 * @param desiredSize
	 *            {@code >= 0;} the desired minimum total size of the array
	 */
	private void ensureCapacity(int desiredSize) {
		if (data.length < desiredSize) {
			byte[] newData = new byte[desiredSize * 2 + 1000];
			System.arraycopy(data, 0, newData, 0, cursor);
			data = newData;
		}
	}

	/**
	 * Annotation on output.
	 */
	private static class Annotation {

		/** {@code >= 0;} start of annotated range (inclusive) */
		private final int start;

		/**
		 * {@code >= 0;} end of annotated range (exclusive);
		 * {@code Integer.MAX_VALUE} if unclosed
		 */
		private int end;

		/** {@code non-null;} annotation text */
		private final String text;

		/**
		 * Constructs an instance.
		 * 
		 * @param start
		 *            {@code >= 0;} start of annotated range
		 * @param end
		 *            {@code >= start;} end of annotated range (exclusive) or
		 *            {@code Integer.MAX_VALUE} if unclosed
		 * @param text
		 *            {@code non-null;} annotation text
		 */
		public Annotation(int start, int end, String text) {
			this.start = start;
			this.end = end;
			this.text = text;
		}

		/**
		 * Constructs an instance. It is initally unclosed.
		 * 
		 * @param start
		 *            {@code >= 0;} start of annotated range
		 * @param text
		 *            {@code non-null;} annotation text
		 */
		public Annotation(int start, String text) {
			this(start, Integer.MAX_VALUE, text);
		}

		/**
		 * Sets the end as given, but only if the instance is unclosed;
		 * otherwise, do nothing.
		 * 
		 * @param end
		 *            {@code >= start;} the end
		 */
		public void setEndIfUnset(int end) {
			if (this.end == Integer.MAX_VALUE) {
				this.end = end;
			}
		}

		/**
		 * Sets the end as given.
		 * 
		 * @param end
		 *            {@code >= start;} the end
		 */
		public void setEnd(int end) {
			this.end = end;
		}

		/**
		 * Gets the start.
		 * 
		 * @return the start
		 */
		public int getStart() {
			return start;
		}

		/**
		 * Gets the end.
		 * 
		 * @return the end
		 */
		public int getEnd() {
			return end;
		}

		/**
		 * Gets the text.
		 * 
		 * @return {@code non-null;} the text
		 */
		public String getText() {
			return text;
		}
	}

	/**
	 * Sets the position of the cursor. If the output is stretchy that capacity
	 * will be expanded if necessary to set the position.
	 * 
	 * @param position
	 *            the position to set to.
	 * @throws IndexOutOfBoundsException
	 *             if this object is not stretchy and position is greater than
	 *             the length of the output.
	 */
	public void setPosition(int position) {
		if (position >= data.length) {
			if (stretchy)
				ensureCapacity(position);
			else
				throw new IndexOutOfBoundsException(
						"this array is not stretchy and cannot be expanded.");
		}

		cursor = position;

	}

}
